#include <gccore.h>
#include <stdio.h>
#include <string.h>
#include <fat.h>
#include <stdlib.h>
#include <wiiuse/wpad.h>
#include "common.h"
#include <sdcard/wiisd_io.h>
#include "IOS_DVD.h"


#define PHYSADDR(x) ((unsigned long *)(0x7FFFFFFF & ((unsigned long)(x))))

static void *xfb = NULL;
static GXRModeObj *rmode = NULL;

int LoadGame()		// DVD Loader! HUGE thanks to crediar who did absolutely all the work.
{
	u8 *buffer=(void*)0x93000000;
	int game_partition_offset=0;

	void	(*app_init)(void (*report)(const char *fmt, ...));
	int		(*app_main)(void **dst, int *size, int *offset);
	void *	(*app_final)();
	void	(*entrypoint)();
	void	(*app_entry)(void(**init)(const char *fmt, ...), int (**main)(), void *(**final)());

	WPAD_Disconnect(0);
	WPAD_Shutdown();
	ISFS_Deinitialize();
	__ES_Close();

	VIDEO_Init();
	
	// This function initialises the attached controllers
	WPAD_Init();
	
	// Obtain the preferred video mode from the system
	// This will correspond to the settings in the Wii menu
	rmode = VIDEO_GetPreferredMode(NULL);
	if (CONF_GetAspectRatio() == CONF_ASPECT_16_9)
	{
		rmode->viWidth = 678;
		rmode->viXOrigin = (VI_MAX_WIDTH_NTSC - 678)/2;
	}	 

	// Allocate memory for the display in the uncached region
	xfb = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode));
	
	// Initialise the console, required for printf
	console_init(xfb,20,20,rmode->fbWidth,rmode->xfbHeight,rmode->fbWidth*VI_DISPLAY_PIX_SZ);
	
	// Set up the video registers with the chosen mode
	VIDEO_Configure(rmode);
	
	// Tell the video hardware where our display memory is
	VIDEO_SetNextFramebuffer(xfb);
	
	// Make the display visible
	VIDEO_SetBlack(FALSE);

	// Flush the video register changes to the hardware
	VIDEO_Flush();

	// Wait for Video setup to complete
	VIDEO_WaitVSync();
	if(rmode->viTVMode&VI_NON_INTERLACE) VIDEO_WaitVSync();

	// TODO: Choose the correct IOS from the games TMD
	//printf("\n");
	//printf("Current IOS: %d\n", (*(vu32*)0x80003140)>>16);
	//printf("IOS_ReloadIOS(): %d\n", IOS_ReloadIOS(36));
	//printf("Current IOS: %d\n", (*(vu32*)0x80003140)>>16);

	printf("\x1b[2;0H\n");

	IOS_ReloadIOS(36);

	DVDInit();
	DVDLowClosePartition();
	DVDLowStopMotor();

	if( DVDLowGetCoverStatus() == DVD_COVER_OPEN )
	{
		printf("No Disc Inserted! Please insert a disc...\nReturning to main menu.");
		sleep(4);
		IOS_Close(DVDGetHandle());
		SYS_ResetSystem(SYS_RESTART,0,0);
		//return -1;
	}
	printf("Disc found! Launching...\n");

	DVDLowReset();
	DVDLowReadDiskID( (void*)0x80000000 );
	DVDLowUnencryptedRead( buffer, 0x20, 0x40000>>2);
	

	DCFlushRange(buffer, 0x100);

	int partitions = ((u32*)buffer)[0];
	int partition_info_offset = ((u32*)buffer)[1] << 2;

	DVDLowUnencryptedRead( buffer, 0x20, partition_info_offset>>2 );

	int i;
	for( i=0; i < partitions; i++)
		if( ((u32*)buffer)[i*2+1] == 0 )
			game_partition_offset = ((u32*)buffer)[i*2]<<2;

	//let's open the partition

	((u32*)buffer)[(0x40>>2)]	=  0x8B000000;
	((u32*)buffer)[(0x40>>2)+1]	=  game_partition_offset>>2;

	//in
	((u32*)buffer)[0x00] = PHYSADDR(buffer+0x40);	//0x00
	((u32*)buffer)[0x01] = 0x20;					//0x04
	((u32*)buffer)[0x02] = 0;						//0x08
	((u32*)buffer)[0x03] = 0x2A4;					//0x0C 
	((u32*)buffer)[0x04] = 0;						//0x10
	((u32*)buffer)[0x05] = 0;						//0x14

	//out
	((u32*)buffer)[0x06] = PHYSADDR(buffer+0x380);	//0x18
	((u32*)buffer)[0x07] = 0x49E4;					//0x1C
	((u32*)buffer)[0x08] = PHYSADDR(buffer+0x360);	//0x20
	((u32*)buffer)[0x09] = 0x20;					//0x24

	DCFlushRange(buffer, 0x100);

	IOS_Ioctlv( DVDGetHandle(), 0x8B, 3, 2, buffer);

	DVDLowRead( buffer, 0x40, 0x20>>2);
	
	DVDLowRead( buffer, 0x20, 0x2440>>2);

	DVDLowRead((void*)0x83200000, *(unsigned long*)(buffer+0x14)+*(unsigned long*)(buffer+0x18), 0x2460>>2);

	app_entry = *(unsigned long*)(buffer+0x10);
	app_entry(&app_init, &app_main, &app_final);
	app_init(printf);

	void *dst=0;
	int lenn, offset;

	while (1)
	{
		lenn=0;
		offset=0;
		dst=0;

		if( app_main(&dst, &lenn, &offset) != 1)
			break;

		DVDLowRead( dst, lenn, offset );
		DCFlushRange(dst, lenn);
	}

	IOS_Close(DVDGetHandle());
	__IOS_ShutdownSubsystems();
	
	entrypoint = app_final();
	
	*(vu32*)0x80000020 = 0x0D15EA5E;				// Magic word (how did the console boot?)
	*(vu32*)0x80000024 = 0x00000001;				// Version (usually set to 1 by apploader)
	*(vu32*)0x80000028 = 0x01800000;				// physical Memory Size
	*(vu32*)0x8000002C = 0x00000023;				// Console type
	*(vu32*)0x80000030 = 0x00000000;				// ArenaLo 
	*(vu32*)0x80000034 = 0x9000FFC0;				// ArenaHi 
	*(vu32*)0x80000040 = 0x00000000;				// Debugger present?
	*(vu32*)0x80000044 = 0x00000000;				// Debugger Exception mask
	*(vu32*)0x80000048 = 0x00000000;				// Exception hook destination 
	*(vu32*)0x8000004C = 0x00000000;				// Temp for LR
	*(vu32*)0x80000050 = 0x00000000;				// padding zeros
	*(vu32*)0x80000060 = 0x38a00040;				// Exception init
	*(vu32*)0x800000E4 = 0x8008f7b8;				// Thread Init
	*(vu32*)0x800000cc = 0x00000005;				// VIInit
	*(vu32*)0x800000F0 = 0x01800000;				// Console Simulated Memory Size
	*(vu32*)0x800000F4 = 0x00000000;				// DVD BI2 location in main memory
	*(vu32*)0x800000F8 = 0x0E7BE2C0;				// Bus Clock Speed
	*(vu32*)0x800000FC = 0x2B73A840;				// CPU Clock Speed
	*(vu32*)0x80003184 = 0x80000000;				// Game ID Address
	*(vu32*)0x80003140 = IOS_GetVersion();			// IOS Version
    *(vu32*)0x80003142 = IOS_GetRevision();		// IOS Revision
	*(vu32*)0x80003144 = 0x00062507;				// IOS Magic


	memcpy((void*)0xC0003180,(void*)0x80000000,4);

	//printf("Switching to:");
	switch(*(vu8*)0x80000003)
	{
		case 'P':
				//printf("PAL60\n");
				*(vu32*)0x800000CC = 0x00000005;
			break;
		case 'J':
		case 'U':
				//printf("NTSC\n");
				*(vu32*)0x800000CC = 0x00000001;
			break;
		default:
				//printf("AUTO\n");
				*(vu32*)0x800000CC = 0x00000000;
			break;
	}

	*(vu32*)0xCC003024 = 0x00000007;
	*(vu32*)0xC00030F0 = 0x0000001C;
	*(vu32*)0xC000318C = 0x00000000;
	*(vu32*)0xC0003190 = 0x00000000;

	//sleep(10);

	entrypoint();
	return 1;
}
